Passed
Branch v8.x (5b95ae)
by Rafael S.
02:16
created

index.js ➔ setCuePoint   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 35
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 6
eloc 27
c 1
b 1
f 0
nc 3
dl 0
loc 35
rs 8.2986
nop 2
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from './vendor/bitdepth.js';
33
import * as imaadpcm from './vendor/imaadpcm.js';
34
import * as alawmulaw from './vendor/alawmulaw.js';
35
import {encode, decode} from './vendor/base64-arraybuffer-es6.js';
36
import {unpackArray, packArrayTo, unpackArrayTo} from './vendor/byte-data.js';
37
import makeWavHeader from './lib/make-wav-header.js';
38
import validateWavHeader from './lib/validate-wav-header';
39
import {riffChunks, findChunk_} from './vendor/riff-chunks.js';
40
import BufferIO from './lib/bufferio.js';
41
import writeWavBuffer from './lib/wav-buffer-writer.js';
42
import readWavBuffer from './lib/wav-buffer-reader.js';
43
import WavBuffer from './lib/wav-buffer.js';
44
45
/**
46
 * Class representing a wav file.
47
 * @extends WavBuffer
48
 * @ignore
49
 */
50
export default class WaveFile extends WavBuffer {
51
52
  /**
53
   * @param {?Uint8Array} bytes A wave file buffer.
54
   * @throws {Error} If no 'RIFF' chunk is found.
55
   * @throws {Error} If no 'fmt ' chunk is found.
56
   * @throws {Error} If no 'data' chunk is found.
57
   */
58
  constructor(bytes=null) {
59
    super();
60
    /**
61
     * The bit depth code according to the samples.
62
     * @type {string}
63
     */
64
    this.bitDepth = '0';
65
    /**
66
     * @type {!Object}
67
     * @private
68
     */
69
    this.dataType = {};
70
    this.io = new BufferIO();
71
    // Load a file from the buffer if one was passed
72
    // when creating the object
73
    if (bytes) {
74
      this.fromBuffer(bytes);
75
    }
76
  }
77
78
  /**
79
   * Set up the WaveFile object based on the arguments passed.
80
   * @param {number} numChannels The number of channels
81
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
82
   * @param {number} sampleRate The sample rate.
83
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
84
   * @param {string} bitDepthCode The audio bit depth code.
85
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
86
   *    or any value between '8' and '32' (like '12').
87
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
88
   *    The samples. Must be in the correct range according to the bit depth.
89
   * @param {?Object} options Optional. Used to force the container
90
   *    as RIFX with {'container': 'RIFX'}
91
   * @throws {Error} If any argument does not meet the criteria.
92
   */
93
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
94
    if (!options.container) {
95
      options.container = 'RIFF';
96
    }
97
    this.container = options.container;
98
    this.bitDepth = bitDepthCode;
99
    samples = this.interleave_(samples);
100
    this.updateDataType_();
101
    /** @type {number} */
102
    let numBytes = this.dataType.bits / 8;
103
    this.data.samples = new Uint8Array(samples.length * numBytes);
104
    packArrayTo(samples, this.dataType, this.data.samples);
105
    /** @type {!Object} */
106
    let header = makeWavHeader(
107
      bitDepthCode, numChannels, sampleRate,
108
      numBytes, this.data.samples.length, options);
109
    this.clearHeader_();
110
    this.chunkSize = header.chunkSize;
111
    this.format = header.format;
112
    this.fmt = header.fmt;
113
    if (header.fact) {
114
      this.fact = header.fact;
115
    }
116
    this.data.chunkId = 'data';
117
    this.data.chunkSize = this.data.samples.length;
118
    validateWavHeader(this);
119
  }
120
121
  /**
122
   * Set up the WaveFile object from a byte buffer.
123
   * @param {!Uint8Array} bytes The buffer.
124
   * @param {boolean=} samples True if the samples should be loaded.
125
   * @throws {Error} If container is not RIFF, RIFX or RF64.
126
   * @throws {Error} If no 'fmt ' chunk is found.
127
   * @throws {Error} If no 'data' chunk is found.
128
   */
129
  fromBuffer(bytes, samples=true) {
130
    this.clearHeader_();
131
    readWavBuffer(bytes, samples, this);
132
    this.bitDepthFromFmt_();
133
    this.updateDataType_();
134
  }
135
136
  /**
137
   * Return a byte buffer representig the WaveFile object as a .wav file.
138
   * The return value of this method can be written straight to disk.
139
   * @return {!Uint8Array} A .wav file.
140
   * @throws {Error} If any property of the object appears invalid.
141
   */
142
  toBuffer() {
143
    validateWavHeader(this);
144
    return writeWavBuffer(this);
145
  }
146
147
  /**
148
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
149
   * @param {string} base64String A .wav file as a base64 string.
150
   * @throws {Error} If any property of the object appears invalid.
151
   */
152
  fromBase64(base64String) {
153
    this.fromBuffer(new Uint8Array(decode(base64String)));
154
  }
155
156
  /**
157
   * Return a base64 string representig the WaveFile object as a .wav file.
158
   * @return {string} A .wav file as a base64 string.
159
   * @throws {Error} If any property of the object appears invalid.
160
   */
161
  toBase64() {
162
    /** @type {!Uint8Array} */
163
    let buffer = this.toBuffer();
164
    return encode(buffer, 0, buffer.length);
165
  }
166
167
  /**
168
   * Return a DataURI string representig the WaveFile object as a .wav file.
169
   * The return of this method can be used to load the audio in browsers.
170
   * @return {string} A .wav file as a DataURI.
171
   * @throws {Error} If any property of the object appears invalid.
172
   */
173
  toDataURI() {
174
    return 'data:audio/wav;base64,' + this.toBase64();
175
  }
176
177
  /**
178
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
179
   * @param {string} dataURI A .wav file as DataURI.
180
   * @throws {Error} If any property of the object appears invalid.
181
   */
182
  fromDataURI(dataURI) {
183
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
184
  }
185
186
  /**
187
   * Force a file as RIFF.
188
   */
189
  toRIFF() {
190
    this.fromScratch(
191
      this.fmt.numChannels,
192
      this.fmt.sampleRate,
193
      this.bitDepth,
194
      unpackArray(this.data.samples, this.dataType));
195
  }
196
197
  /**
198
   * Force a file as RIFX.
199
   */
200
  toRIFX() {
201
    this.fromScratch(
202
      this.fmt.numChannels,
203
      this.fmt.sampleRate,
204
      this.bitDepth,
205
      unpackArray(this.data.samples, this.dataType),
206
      {container: 'RIFX'});
207
  }
208
209
  /**
210
   * Change the bit depth of the samples.
211
   * @param {string} newBitDepth The new bit depth of the samples.
212
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
213
   * @param {boolean} changeResolution A boolean indicating if the
214
   *    resolution of samples should be actually changed or not.
215
   * @throws {Error} If the bit depth is not valid.
216
   */
217
  toBitDepth(newBitDepth, changeResolution=true) {
218
    /** @type {string} */
219
    let toBitDepth = newBitDepth;
220
    /** @type {string} */
221
    let thisBitDepth = this.bitDepth;
222
    if (!changeResolution) {
223
      if (newBitDepth != '32f') {
224
        toBitDepth = this.dataType.bits.toString();
225
      }
226
      thisBitDepth = this.dataType.bits;
227
    }
228
    this.assureUncompressed_();
229
    /** @type {number} */
230
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
231
    /** @type {!Float64Array} */
232
    let typedSamplesInput = new Float64Array(sampleCount + 1);
233
    /** @type {!Float64Array} */
234
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
235
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
236
    bitDepthLib(
237
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
238
    this.fromScratch(
239
      this.fmt.numChannels,
240
      this.fmt.sampleRate,
241
      newBitDepth,
242
      typedSamplesOutput,
243
      {container: this.correctContainer_()});
244
  }
245
246
  /**
247
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
248
   * @throws {Error} If sample rate is not 8000.
249
   * @throws {Error} If number of channels is not 1.
250
   */
251
  toIMAADPCM() {
252
    if (this.fmt.sampleRate !== 8000) {
253
      throw new Error(
254
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
255
    } else if (this.fmt.numChannels !== 1) {
256
      throw new Error(
257
        'Only mono files can be compressed as IMA-ADPCM.');
258
    } else {
259
      this.assure16Bit_();
260
      let output = new Int16Array(this.data.samples.length / 2);
261
      unpackArrayTo(this.data.samples, this.dataType, output);
262
      this.fromScratch(
263
        this.fmt.numChannels,
264
        this.fmt.sampleRate,
265
        '4',
266
        imaadpcm.encode(output),
267
        {container: this.correctContainer_()});
268
    }
269
  }
270
271
  /**
272
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
273
   * @param {string} bitDepthCode The new bit depth of the samples.
274
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
275
   *    Optional. Default is 16.
276
   */
277
  fromIMAADPCM(bitDepthCode='16') {
278
    this.fromScratch(
279
      this.fmt.numChannels,
280
      this.fmt.sampleRate,
281
      '16',
282
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
283
      {container: this.correctContainer_()});
284
    if (bitDepthCode != '16') {
285
      this.toBitDepth(bitDepthCode);
286
    }
287
  }
288
289
  /**
290
   * Encode a 16-bit wave file as 8-bit A-Law.
291
   */
292
  toALaw() {
293
    this.assure16Bit_();
294
    let output = new Int16Array(this.data.samples.length / 2);
295
    unpackArrayTo(this.data.samples, this.dataType, output);
296
    this.fromScratch(
297
      this.fmt.numChannels,
298
      this.fmt.sampleRate,
299
      '8a',
300
      alawmulaw.alaw.encode(output),
301
      {container: this.correctContainer_()});
302
  }
303
304
  /**
305
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
306
   * @param {string} bitDepthCode The new bit depth of the samples.
307
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
308
   *    Optional. Default is 16.
309
   */
310
  fromALaw(bitDepthCode='16') {
311
    this.fromScratch(
312
      this.fmt.numChannels,
313
      this.fmt.sampleRate,
314
      '16',
315
      alawmulaw.alaw.decode(this.data.samples),
316
      {container: this.correctContainer_()});
317
    if (bitDepthCode != '16') {
318
      this.toBitDepth(bitDepthCode);
319
    }
320
  }
321
322
  /**
323
   * Encode 16-bit wave file as 8-bit mu-Law.
324
   */
325
  toMuLaw() {
326
    this.assure16Bit_();
327
    let output = new Int16Array(this.data.samples.length / 2);
328
    unpackArrayTo(this.data.samples, this.dataType, output);
329
    this.fromScratch(
330
      this.fmt.numChannels,
331
      this.fmt.sampleRate,
332
      '8m',
333
      alawmulaw.mulaw.encode(output),
334
      {container: this.correctContainer_()});
335
  }
336
337
  /**
338
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
339
   * @param {string} bitDepthCode The new bit depth of the samples.
340
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
341
   *    Optional. Default is 16.
342
   */
343
  fromMuLaw(bitDepthCode='16') {
344
    this.fromScratch(
345
      this.fmt.numChannels,
346
      this.fmt.sampleRate,
347
      '16',
348
      alawmulaw.mulaw.decode(this.data.samples),
349
      {container: this.correctContainer_()});
350
    if (bitDepthCode != '16') {
351
      this.toBitDepth(bitDepthCode);
352
    }
353
  }
354
355
  /**
356
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
357
   * then it is created. It if exists, it is overwritten.
358
   * @param {string} tag The tag name.
359
   * @param {string} value The tag value.
360
   * @throws {Error} If the tag name is not valid.
361
   */
362
  setTag(tag, value) {
363
    tag = this.fixTagName_(tag);
364
    /** @type {!Object} */
365
    let index = this.getTagIndex_(tag);
366
    if (index.TAG !== null) {
367
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
368
        value.length + 1;
369
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
370
    } else if (index.LIST !== null) {
371
      this.LIST[index.LIST].subChunks.push({
372
        chunkId: tag,
373
        chunkSize: value.length + 1,
374
        value: value});
375
    } else {
376
      this.LIST.push({
377
        chunkId: 'LIST',
378
        chunkSize: 8 + value.length + 1,
379
        format: 'INFO',
380
        subChunks: []});
381
      this.LIST[this.LIST.length - 1].subChunks.push({
382
        chunkId: tag,
383
        chunkSize: value.length + 1,
384
        value: value});
385
    }
386
  }
387
388
  /**
389
   * Return the value of a RIFF tag in the INFO chunk.
390
   * @param {string} tag The tag name.
391
   * @return {?string} The value if the tag is found, null otherwise.
392
   */
393
  getTag(tag) {
394
    /** @type {!Object} */
395
    let index = this.getTagIndex_(tag);
396
    if (index.TAG !== null) {
397
      return this.LIST[index.LIST].subChunks[index.TAG].value;
398
    }
399
    return null;
400
  }
401
402
  /**
403
   * Return a Object<tag, value> with the RIFF tags in the file.
404
   * @return {!Object<string, string>} The file tags.
405
   */
406
  listTags() {
407
    /** @type {?number} */
408
    let index = this.getLISTINFOIndex_();
409
    /** @type {!Object} */
410
    let tags = {};
411
    if (index !== null) {
412
      for (let i=0; i<this.LIST[index].subChunks.length; i++) {
413
        tags[this.LIST[index].subChunks[i].chunkId] =
414
          this.LIST[index].subChunks[i].value;
415
      }
416
    }
417
    return tags;
418
  }
419
420
  /**
421
   * Remove a RIFF tag in the INFO chunk.
422
   * @param {string} tag The tag name.
423
   * @return {boolean} True if a tag was deleted.
424
   */
425
  deleteTag(tag) {
426
    /** @type {!Object} */
427
    let index = this.getTagIndex_(tag);
428
    if (index.TAG !== null) {
429
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
430
      return true;
431
    }
432
    return false;
433
  }
434
435
  /**
436
   * Create a cue point in the wave file.
437
   * @param {number} position The cue point position in milliseconds.
438
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
439
   */
440
  setCuePoint(position, labl='') {
441
    this.cue.chunkId = 'cue ';
442
    position = (position * this.fmt.sampleRate) / 1000;
443
    /** @type {!Array<!Object>} */
444
    let existingPoints = this.getCuePoints_();
445
    this.clearLISTadtl_();
446
    /** @type {number} */
447
    let len = this.cue.points.length;
448
    this.cue.points = [];
449
    /** @type {boolean} */
450
    let hasSet = false;
451
    if (len === 0) {
452
      this.setCuePoint_(position, 1, labl);
453
    } else {
454
      for (let i=0; i<len; i++) {
455
        if (existingPoints[i].dwPosition > position && !hasSet) {
456
          this.setCuePoint_(position, i + 1, labl);
457
          this.setCuePoint_(
458
            existingPoints[i].dwPosition,
459
            i + 2,
460
            existingPoints[i].label);
461
          hasSet = true;
462
        } else {
463
          this.setCuePoint_(
464
            existingPoints[i].dwPosition,
465
            i + 1,
466
            existingPoints[i].label);
467
        }
468
      }
469
      if (!hasSet) {
470
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
471
      }
472
    }
473
    this.cue.dwCuePoints = this.cue.points.length;
474
  }
475
476
  /**
477
   * Remove a cue point from a wave file.
478
   * @param {number} index the index of the point. First is 1,
479
   *    second is 2, and so on.
480
   */
481
  deleteCuePoint(index) {
482
    this.cue.chunkId = 'cue ';
483
    /** @type {!Array<!Object>} */
484
    let existingPoints = this.getCuePoints_();
485
    this.clearLISTadtl_();
486
    /** @type {number} */
487
    let len = this.cue.points.length;
488
    this.cue.points = [];
489
    for (let i=0; i<len; i++) {
490
      if (i + 1 !== index) {
491
        this.setCuePoint_(
492
          existingPoints[i].dwPosition,
493
          i + 1,
494
          existingPoints[i].label);
495
      }
496
    }
497
    this.cue.dwCuePoints = this.cue.points.length;
498
    if (this.cue.dwCuePoints) {
499
      this.cue.chunkId = 'cue ';
500
    } else {
501
      this.cue.chunkId = '';
502
      this.clearLISTadtl_();
503
    }
504
  }
505
506
  /**
507
   * Return an array with all cue points in the file, in the order they appear
508
   * in the file.
509
   * The difference between this method and using the list in WaveFile.cue
510
   * is that the return value of this method includes the position in
511
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
512
   * @return {!Array<!Object>}
513
   * @private
514
   */
515
  listCuePoints() {
516
    /** @type {!Array<!Object>} */
517
    let points = this.getCuePoints_();
518
    for (let i=0; i<points.length; i++) {
519
      points[i].milliseconds =
520
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
521
    }
522
    return points;
523
  }
524
525
  /**
526
   * Update the label of a cue point.
527
   * @param {number} pointIndex The ID of the cue point.
528
   * @param {string} label The new text for the label.
529
   */
530
  updateLabel(pointIndex, label) {
531
    /** @type {?number} */
532
    let adtlIndex = this.getAdtlChunk_();
533
    if (adtlIndex !== null) {
534
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
535
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
536
            pointIndex) {
537
          this.LIST[adtlIndex].subChunks[i].value = label;
538
        }
539
      }
540
    }
541
  }
542
543
  /**
544
   * Set the string code of the bit depth based on the 'fmt ' chunk.
545
   * @private
546
   */
547
  bitDepthFromFmt_() {
548
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
549
      this.bitDepth = '32f';
550
    } else if (this.fmt.audioFormat === 6) {
551
      this.bitDepth = '8a';
552
    } else if (this.fmt.audioFormat === 7) {
553
      this.bitDepth = '8m';
554
    } else {
555
      this.bitDepth = this.fmt.bitsPerSample.toString();
556
    }
557
  }
558
  
559
  /**
560
   * Push a new cue point in this.cue.points.
561
   * @param {number} position The position in milliseconds.
562
   * @param {number} dwName the dwName of the cue point
563
   * @private
564
   */
565
  setCuePoint_(position, dwName, label) {
566
    this.cue.points.push({
567
      dwName: dwName,
568
      dwPosition: position,
569
      fccChunk: 'data',
570
      dwChunkStart: 0,
571
      dwBlockStart: 0,
572
      dwSampleOffset: position,
573
    });
574
    this.setLabl_(dwName, label);
575
  }
576
577
  /**
578
   * Return an array with all cue points in the file, in the order they appear
579
   * in the file.
580
   * @return {!Array<!Object>}
581
   * @private
582
   */
583
  getCuePoints_() {
584
    /** @type {!Array<!Object>} */
585
    let points = [];
586
    for (let i=0; i<this.cue.points.length; i++) {
587
      points.push({
588
        dwPosition: this.cue.points[i].dwPosition,
589
        label: this.getLabelForCuePoint_(
590
          this.cue.points[i].dwName)});
591
    }
592
    return points;
593
  }
594
595
  /**
596
   * Return the label of a cue point.
597
   * @param {number} pointDwName The ID of the cue point.
598
   * @return {string}
599
   * @private
600
   */
601
  getLabelForCuePoint_(pointDwName) {
602
    /** @type {?number} */
603
    let adtlIndex = this.getAdtlChunk_();
604
    if (adtlIndex !== null) {
605
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
606
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
607
            pointDwName) {
608
          return this.LIST[adtlIndex].subChunks[i].value;
609
        }
610
      }
611
    }
612
    return '';
613
  }
614
615
  /**
616
   * Clear any LIST chunk labeled as 'adtl'.
617
   * @private
618
   */
619
  clearLISTadtl_() {
620
    for (let i=0; i<this.LIST.length; i++) {
621
      if (this.LIST[i].format == 'adtl') {
622
        this.LIST.splice(i);
623
      }
624
    }
625
  }
626
627
  /**
628
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
629
   * @param {number} dwName The ID of the cue point.
630
   * @param {string} label The label for the cue point.
631
   * @private
632
   */
633
  setLabl_(dwName, label) {
634
    /** @type {?number} */
635
    let adtlIndex = this.getAdtlChunk_();
636
    if (adtlIndex === null) {
637
      this.LIST.push({
638
        chunkId: 'LIST',
639
        chunkSize: 4,
640
        format: 'adtl',
641
        subChunks: []});
642
      adtlIndex = this.LIST.length - 1;
643
    }
644
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
645
  }
646
647
  /**
648
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
649
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
650
   * @param {number} dwName The ID of the cue point.
651
   * @param {string} label The label for the cue point.
652
   * @private
653
   */
654
  setLabelText_(adtlIndex, dwName, label) {
655
    this.LIST[adtlIndex].subChunks.push({
656
      chunkId: 'labl',
657
      chunkSize: label.length,
658
      dwName: dwName,
659
      value: label
660
    });
661
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
662
  }
663
664
  /**
665
   * Return the index of the 'adtl' LIST in this.LIST.
666
   * @return {?number}
667
   * @private
668
   */
669
  getAdtlChunk_() {
670
    for (let i=0; i<this.LIST.length; i++) {
671
      if (this.LIST[i].format == 'adtl') {
672
        return i;
673
      }
674
    }
675
    return null;
676
  }
677
678
  /**
679
   * Return the index of the INFO chunk in the LIST chunk.
680
   * @return {?number} the index of the INFO chunk.
681
   * @private
682
   */
683
  getLISTINFOIndex_() {
684
    /** @type {?number} */
685
    let index = null;
686
    for (let i=0; i<this.LIST.length; i++) {
687
      if (this.LIST[i].format === 'INFO') {
688
        index = i;
689
        break;
690
      }
691
    }
692
    return index;
693
  }
694
695
  /**
696
   * Return the index of a tag in a FILE chunk.
697
   * @param {string} tag The tag name.
698
   * @return {!Object<string, ?number>}
699
   *    Object.LIST is the INFO index in LIST
700
   *    Object.TAG is the tag index in the INFO
701
   * @private
702
   */
703
  getTagIndex_(tag) {
704
    /** @type {!Object<string, ?number>} */
705
    let index = {LIST: null, TAG: null};
706
    for (let i=0; i<this.LIST.length; i++) {
707
      if (this.LIST[i].format == 'INFO') {
708
        index.LIST = i;
709
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
710
          if (this.LIST[i].subChunks[j].chunkId == tag) {
711
            index.TAG = j;
712
            break;
713
          }
714
        }
715
        break;
716
      }
717
    }
718
    return index;
719
  }
720
721
  /**
722
   * Fix a RIFF tag format if possible, throw an error otherwise.
723
   * @param {string} tag The tag name.
724
   * @return {string} The tag name in proper fourCC format.
725
   * @private
726
   */
727
  fixTagName_(tag) {
728
    if (tag.constructor !== String) {
729
      throw new Error('Invalid tag name.');
730
    } else if (tag.length < 4) {
731
      for (let i=0; i<4-tag.length; i++) {
732
        tag += ' ';
733
      }
734
    }
735
    return tag;
736
  }
737
738
  /**
739
   * Reset attributes that should emptied when a file is
740
   * created with the fromScratch() or fromBuffer() methods.
741
   * @private
742
   */
743
  clearHeader_() {
744
    this.fmt.cbSize = 0;
745
    this.fmt.validBitsPerSample = 0;
746
    this.fact.chunkId = '';
747
    this.ds64.chunkId = '';
748
  }
749
750
  /**
751
   * Make the file 16-bit if it is not.
752
   * @private
753
   */
754
  assure16Bit_() {
755
    this.assureUncompressed_();
756
    if (this.bitDepth != '16') {
757
      this.toBitDepth('16');
758
    }
759
  }
760
761
  /**
762
   * Uncompress the samples in case of a compressed file.
763
   * @private
764
   */
765
  assureUncompressed_() {
766
    if (this.bitDepth == '8a') {
767
      this.fromALaw();
768
    } else if (this.bitDepth == '8m') {
769
      this.fromMuLaw();
770
    } else if (this.bitDepth == '4') {
771
      this.fromIMAADPCM();
772
    }
773
  }
774
775
  /**
776
   * Set up the WaveFile object from a byte buffer.
777
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
778
   * @private
779
   */
780
  interleave_(samples) {
781
    if (samples.length > 0) {
782
      if (samples[0].constructor === Array) {
783
        /** @type {!Array<number>} */
784
        let finalSamples = [];
785
        for (let i=0; i < samples[0].length; i++) {
786
          for (let j=0; j < samples.length; j++) {
787
            finalSamples.push(samples[j][i]);
788
          }
789
        }
790
        samples = finalSamples;
791
      }
792
    }
793
    return samples;
794
  }
795
796
  /**
797
   * Update the type definition used to read and write the samples.
798
   * @private
799
   */
800
  updateDataType_() {
801
    /** @type {!Object} */
802
    this.dataType = {
803
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
804
      float: this.bitDepth == '32f' || this.bitDepth == '64',
805
      signed: this.bitDepth != '8',
806
      be: this.container == 'RIFX'
807
    };
808
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
809
      this.dataType.bits = 8;
810
      this.dataType.signed = false;
811
    }
812
  }
813
814
  /**
815
   * Return 'RIFF' if the container is 'RF64', the current container name
816
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
817
   * @return {string}
818
   * @private
819
   */
820
  correctContainer_() {
821
    return this.container == 'RF64' ? 'RIFF' : this.container;
822
  }
823
}
824